﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Progresoft.Common.Database;
using Progresoft.Scheduler.RemoteInterface.DataAccess;

using NHibernate;
using NHibernate.Criterion;
using System.Collections;
using System.Xml.Serialization;
using System.IO;
using System.Xml;
using System.Xml.Xsl;
using System.Reflection;
using System.Xml.Schema;

namespace Progresoft.Scheduler.RemoteInterface.Api
{
	/// <summary>
	/// Класс для ремоутинг-интерфейса шедулера
	/// </summary>
	public class SchedulerServer : MarshalByRefObject
	{
		/// <summary>
		/// Путь к конфигу
		/// </summary>
		public static string nHibernateConfigPath;

		/// <summary>
		/// Открыть сессию, получив фабрику из кеша.
		/// </summary>
		private ISession OpenSession()
		{
			if (string.IsNullOrEmpty(nHibernateConfigPath))
				throw new Exception("не задан путь для конфигурационного файла.");

			return SessionFactorySingleton
				.GetSessionFactory(nHibernateConfigPath)
				.OpenSession();
		}

		/// <summary>
		/// Получить список доступных систем
		/// </summary>
		/// <returns></returns>
		public string[] ListClientSystemKeys()
		{
			using (ISession session = this.OpenSession())
			{
				return session.CreateCriteria(typeof(ClientSystem))
					.List<ClientSystem>()
					.Select(item => item.Name)
					.ToArray();
			}
		}

		/// <summary>
		/// Получить данные для дэшборда
		/// </summary>
		/// <param name="clientSystem">Опциональный параметр -- имя клиентской системы. Если null, то данные на всех.</param>
		public DashboardData GetDashboardData(string clientSystem)
		{
			using (ISession session = this.OpenSession())
			{
				//получить список периодических задач с учетом системы
				ICriteria periodicTaskCtiteria = session.CreateCriteria(typeof(PeriodicTask));
				if (!string.IsNullOrEmpty(clientSystem))
					periodicTaskCtiteria
						.CreateCriteria(PeriodicTask.TaskProp)
							.CreateCriteria(TaskData.SystemProp)
								.Add(Expression.Eq(ClientSystem.NameProp, clientSystem));
				var periodicTaskList = periodicTaskCtiteria.List<PeriodicTask>();

				//результирующий список сущностей, описыающих периодические задачи
				var periodic = (from periodicTask in periodicTaskList
								select new PeriodicTaskHealth()
								{
									TaskName = periodicTask.Task.Name,
									Period = TimeSpan.FromSeconds(periodicTask.AttemptPeriod),
									SuccessfullyFinishedAt = periodicTask.Task.LastSuccessful,
									Status = this.GetPeriodicTaskStatus(periodicTask),
									LogID = periodicTask.TaskID,
									System = periodicTask.Task.System.Name,
									Key = periodicTask.Task.KeyName
								}).ToList();

				//получить список задач по расписанию. т.е задач для которых нет периодических задач
				int[] periodicTaskIDList = (from periodicTask in periodicTaskList
											select periodicTask.TaskID).ToArray();
				ICriteria scheduledTaskCriteria = session.CreateCriteria(typeof(TaskData))
					.Add(Expression.Not(Expression.In(TaskData.IDProp, periodicTaskIDList)));
				if (!string.IsNullOrEmpty(clientSystem))
					scheduledTaskCriteria
						.CreateCriteria(TaskData.SystemProp)
								.Add(Expression.Eq(ClientSystem.NameProp, clientSystem));

				var scheduledTaskList = scheduledTaskCriteria.List<TaskData>();
				var scheduledTaskIDList = (from scheduledTask in scheduledTaskList
										   select scheduledTask.ID).ToArray();

				//результирующий список сущностей, описывающий задачи по расписанию
				List<ScheduledTaskHealth> scheduled = new List<ScheduledTaskHealth>();
				// это чтобы не убить дерьмовый денисовский код переделкой
				// на самом деле, это список обработанных ИДшников всех задач
				List<int> processedTaskIDs = new List<int>();

				//для задач по расписанию выявить задачи, имеющие фиктивное состояние Running или Delayed
				List<TaskEventState> runningStateList = new List<TaskEventState>();
				runningStateList.Add(TaskEventState.Created);
				runningStateList.Add(TaskEventState.Running);
				runningStateList.Add(TaskEventState.Delayed);
				var runningTaskEventList = session.CreateCriteria(typeof(TaskEvent))
					.Add(Expression.Le(TaskEvent.StartedAtProp, DateTime.Now))
					.Add(Expression.In(TaskEvent.TaskIDProp, scheduledTaskIDList))
					.Add(Expression.In(TaskEvent.StateProp, runningStateList))
					.List<TaskEvent>();

				//добавить в результирующий список запущенные задачи
				RunningTaskEventComparer comparer = new RunningTaskEventComparer();
				processedTaskIDs.AddRange((from task in runningTaskEventList select task.TaskID).Distinct());
				scheduled.AddRange((from task in runningTaskEventList
									group task by task.Task into gropedList
									select new ScheduledTaskHealth()
									{
										TaskName = gropedList.Key.Name,
										Status = this.GetScheduledTaskStatus(gropedList
											.OrderBy(item => item, comparer)
											.FirstOrDefault().State),
										SuccessfullyFinishedAt = gropedList
											.OrderBy(item => item, comparer)
											.FirstOrDefault()
											.Task.LastSuccessful,
										StartDate = gropedList
											.OrderBy(item => item, comparer)
											.FirstOrDefault()
											.ScheduledTime,
										LogID = gropedList
											.OrderBy(item => item, comparer)
											.FirstOrDefault()
											.ID,
										SystemName = gropedList.Key.System.Name,
										Key = gropedList.Key.KeyName
									}).ToList());

				//получить список идшников необработанных задач по расписанию
				int[] notProcessedTaskIDList = this.DistractListProcessedIDs(processedTaskIDs, scheduledTaskIDList);

				//для задач по расписанию выявить задачи, имеющие фиктивное состояние OK, Cancelled или Failed
				//select * 
				//from TASK_EVENT as t1
				//where FIRST_STARTED_AT = 
				//    (select MAX(FIRST_STARTED_AT) 
				//    from TASK_EVENT as t2
				//    where t2.TASK_ID = t1.TASK_ID)
				DetachedCriteria subQuery = DetachedCriteria.For<TaskEvent>("te")
					.Add(Expression.Le(TaskEvent.ScheduledTimeProp, DateTime.Now))
					.Add(Expression.In(TaskEvent.TaskIDProp, notProcessedTaskIDList))
					.SetProjection(Projections.Max(TaskEvent.ScheduledTimeProp))
					.Add(Expression.EqProperty("te." + TaskEvent.TaskIDProp,
						"te2." + TaskEvent.TaskIDProp));

				var completedTaskEventList = session.CreateCriteria(typeof(TaskEvent), "te2")
					.Add(Expression.Le(TaskEvent.ScheduledTimeProp, DateTime.Now))
					.Add(Expression.In(TaskEvent.TaskIDProp, notProcessedTaskIDList))
					.Add(Subqueries.PropertyEq(TaskEvent.ScheduledTimeProp, subQuery))
					.List<TaskEvent>();

				//добавить в результирующий список завершенные задачи
				processedTaskIDs.AddRange((from taskEvent in completedTaskEventList select taskEvent.TaskID).Distinct());
				scheduled.AddRange((from taskEvent in completedTaskEventList
									select new ScheduledTaskHealth()
									{
										LogID = taskEvent.ID,
										StartDate = taskEvent.StartedAt,
										Status = this.GetScheduledTaskStatus(taskEvent.State),
										SuccessfullyFinishedAt = taskEvent.Task.LastSuccessful,
										TaskName = taskEvent.Task.Name,
										SystemName = taskEvent.Task.System.Name,
										Key = taskEvent.Task.KeyName
									}).ToList());

				//получить список идшников необработанных задач по расписанию
				notProcessedTaskIDList = this.DistractListProcessedIDs(processedTaskIDs, scheduledTaskIDList);

				//добавить в результирующий набор задачи, о которых мы ничего сказать не можем
				if (notProcessedTaskIDList.Length > 0)
					scheduled.AddRange((from task in scheduledTaskList
										where notProcessedTaskIDList.Contains(task.ID)
										select new ScheduledTaskHealth()
										{
											TaskName = task.Name,
											Status = ScheduledTaskStatus.Unknown,
											SuccessfullyFinishedAt = null,
											StartDate = null,
											LogID = null,
											SystemName = task.System.Name,
											Key = task.KeyName
										}).ToList());

				DashboardData data = new DashboardData(scheduled.OrderBy(item => item.TaskName).ToList(), periodic);
				return data;
			}
		}

		/// <summary>
		/// получить задачу по расписанию
		/// </summary>
		private TaskEvent GetTaskEvent(ISession session, int taskID)
		{
			TaskEvent task = session.CreateCriteria(typeof(TaskEvent))
					.Add(Expression.Eq(TaskEvent.IDProp, taskID))
					.SetMaxResults(1)
					.UniqueResult<TaskEvent>();

			if (task == null)
				throw new Exception("задача по расписанию не найдена");

			return task;
		}

		/// <summary>
		/// получить данные по задаче
		/// </summary>
		private TaskData GetTaskData(ISession session, string systemName, string taskKey)
		{
			var task = session.CreateCriteria(typeof(TaskData))
				.CreateAlias(TaskData.SystemProp, "system")
				.Add(Expression.Eq("system." + ClientSystem.NameProp, systemName))
				.Add(Expression.Eq(TaskData.KeyNameProp, taskKey))
				.UniqueResult<TaskData>();

			if (task == null)
				throw new Exception("задача не найдена");

			return task;
		}

		/// <summary>
		/// получить периодическую задачу
		/// </summary>
		private PeriodicTask GetPeriodicTask(ISession session, int taskID)
		{
			var task = session.CreateCriteria(typeof(PeriodicTask))
					.Add(Expression.Eq(PeriodicTask.TaskIDProp, taskID))
					.SetMaxResults(1)
					.UniqueResult<PeriodicTask>();

			if (task == null)
				throw new Exception("периодическая задача не найдена");

			return task;
		}

		/// <summary>
		/// получить фиктивное состояние периодической задачи
		/// </summary>
		private PeriodicTaskStatus GetPeriodicTaskStatus(PeriodicTask task)
		{
			if (task.IsEnabled)
			{
				//если у задачи нет лога, то она находится в состоянии Unknown
				//случай когда был успешный запуск, но нет лога мы не расcматриваем
				if (string.IsNullOrEmpty(task.Log))
					return PeriodicTaskStatus.Unknown;
				{
					//если последний успешный запуск задачи был более двух периодов запуска назад,
					//считаем что задача зафейлилась
					if (!task.Task.LastSuccessful.HasValue ||
						task.Task.LastSuccessful.Value.AddSeconds(2 * task.AttemptPeriod) < DateTime.Now)
						return PeriodicTaskStatus.Failed;
					else
						return PeriodicTaskStatus.OK;
				}
			}
			else
				return PeriodicTaskStatus.Disabled;
		}

		/// <summary>
		/// Получить фиктивное состоянии задачи по расписнию
		/// </summary>
		private ScheduledTaskStatus GetScheduledTaskStatus(TaskEventState status)
		{
			switch (status)
			{
				case TaskEventState.Successful:
					return ScheduledTaskStatus.OK;
				case TaskEventState.Cancelled:
					return ScheduledTaskStatus.Cancelled;
				case TaskEventState.Failed:
					return ScheduledTaskStatus.Failed;
				case TaskEventState.Delayed:
					return ScheduledTaskStatus.Delayed;
				case TaskEventState.Running:
				case TaskEventState.Created:
					return ScheduledTaskStatus.Running;
				default:
					throw new Exception("недопустимый статус задачи");
			}
		}

		/// <summary>
		/// получить массив идшников задач по расписанию, для которых мы еще не установили состояние
		/// </summary>
		private int[] DistractListProcessedIDs(List<int> processedTaskIDList,
			int[] fullListIDs)
		{
			int[] notProcessedTaskIDList = (from taskID in fullListIDs
											where !processedTaskIDList.Contains(taskID)
											select taskID).ToArray();
			return notProcessedTaskIDList;
		}

		/// <summary>
		/// Компарер TaskEvent'ов для для запущенных задач
		/// Delayed > (Running || Created)
		/// </summary>
		private class RunningTaskEventComparer : IComparer<TaskEvent>
		{
			public int Compare(TaskEvent x, TaskEvent y)
			{
				if (x == null) return (y == null) ? 0 : 1;
				if (y == null) return -1;

				if ((x.State == TaskEventState.Delayed && y.State == TaskEventState.Delayed)
					|| (x.State != TaskEventState.Delayed && y.State != TaskEventState.Delayed))
					return 0;
				else
				{
					if (x.State == TaskEventState.Delayed)
						return 1;
					else
						return -1;
				}
			}
		}

		/// <summary>
		/// Получить лог задачи
		/// </summary>
		[Obsolete("Денис ты гандон! Удалить нахуй этот метод! -> Идите нахуй батенька, не моя идея")]
		public string GetTaskLog(int logID, string type, out string taskName)
		{
			TaskData task;
			string log = GetTaskLog(logID, (TaskType)Enum.Parse(typeof(TaskType), type, true), out task);
			taskName = task.Name;
			return log;
		}

		/// <summary>
		/// Получить лог задачи
		/// </summary>
		public string GetTaskLog(int logID, TaskType type, out TaskData task)
		{
			using (ISession session = this.OpenSession())
			{
				switch (type)
				{
					case TaskType.Periodic:
						PeriodicTask periodicTask = this.GetPeriodicTask(session, logID);
						task = periodicTask.Task;
						return periodicTask.Log;
					case TaskType.Scheduled:
						TaskEvent scheduledTask = this.GetTaskEvent(session, logID);
						task = scheduledTask.Task;
						return scheduledTask.Log;
					default:
						throw new Exception("параметр type имеет недопустимое значение");
				}
			}
		}

		/// <summary>
		/// Детальная информация по задаче по расписанию
		/// </summary>
		public ScheduledTaskInfo GetScheduledTaskInfo(string systemName, string taskKey, out IList<TaskSchedule> schedules)
		{
			using (ISession session = this.OpenSession())
			{
				TaskData task = this.GetTaskData(session, systemName, taskKey);
				schedules = session.CreateCriteria(typeof(TaskSchedule))
					.Add(Expression.Eq(TaskSchedule.TaskIDProp, task.ID))
					.List<TaskSchedule>();

				return new ScheduledTaskInfo()
				{
					ID = task.ID,
					SuccessfullyFinishedAt = task.LastSuccessful,
					TaskName = task.Name,
					Assembly = task.Assembly,
					Class = task.TaskClassName,
					Key = task.KeyName,
					Description = task.Description,
					System = task.System.Name
				};
			}
		}

		/// <summary>
		/// Преобразовать список TaskEvent в список ScheduledTaskEvent
		/// </summary>
		/// <param name="taskEventList"></param>
		private List<ScheduledTaskEvent> ConvertListTaskEvents(IList<TaskEvent> taskEventList)
		{
			return (from taskEvent in taskEventList
					select new ScheduledTaskEvent()
					{
						LogID = string.IsNullOrEmpty(taskEvent.Log) ? null : (int?)taskEvent.ID,
						StartedAt = taskEvent.StartedAt.HasValue ? taskEvent.StartedAt : taskEvent.ScheduledTime,
						Status = taskEvent.State,
						ClientSystem = taskEvent.Task.System.Name,
						ValidUntil = taskEvent.ValidUntil,
						Initiator = taskEvent.Initiator,
						FinishedAt = taskEvent.ChangedAt,
						IsManual = taskEvent.IsManual
					}).ToList();
		}

		/// <summary>
		/// Создает критерий для списка запусков
		/// </summary>
		private ICriteria GetListScheduledTaskEventsQuery(ISession session,
			int taskID, DateTime? startedDateFrom, DateTime? startedDateTo,
			TaskEventState? taskState, Boolean? isManual)
		{
			ICriteria criteria = session.CreateCriteria(typeof(TaskEvent))
					.Add(Expression.Eq(TaskEvent.TaskIDProp, taskID));
			if (startedDateFrom.HasValue)
				criteria.Add(Expression.Ge(TaskEvent.StartedAtProp, startedDateFrom.Value));
			if (startedDateTo.HasValue)
				criteria.Add(Expression.Le(TaskEvent.StartedAtProp, startedDateTo.Value));
			if (taskState.HasValue)
				criteria.Add(Expression.Eq(TaskEvent.StateProp, taskState.Value));
			if (isManual.HasValue)
				criteria.Add(Expression.Eq(TaskEvent.IsManualProp, isManual.Value));
			return criteria;
		}

		/// <summary>
		/// Список задач
		/// </summary>
		public List<ScheduledTaskEvent> ListScheduledTaskEvents(string systemName, string taskKey,
			DateTime? startedDateFrom, DateTime? startedDateTo,
			TaskEventState? taskState, Boolean? isManual,
			String sortingExpression, int pageSize, int startIndex)
		{
			using (ISession session = this.OpenSession())
			{
				TaskData task = GetTaskData(session, systemName, taskKey);
				ICriteria taskEventListCriteria =
					GetListScheduledTaskEventsQuery(session, task.ID, startedDateFrom,
					startedDateTo, taskState, isManual);
				taskEventListCriteria.SetPadingAndOrder(
					sortingExpression, TaskEvent.DueTimeProp + " DESC", pageSize, startIndex);
				IList<TaskEvent> taskEventList = taskEventListCriteria.List<TaskEvent>();
				List<ScheduledTaskEvent> result = this.ConvertListTaskEvents(taskEventList);
				return result;
			}
		}

		/// <summary>
		/// Каунтер запусков задач
		/// </summary>
		public int ListScheduledTaskEventsCount(string systemName, string taskKey,
			DateTime? startedDateFrom, DateTime? startedDateTo,
			TaskEventState? taskState, Boolean? isManual)
		{
			using (ISession session = this.OpenSession())
			{
				TaskData task = GetTaskData(session, systemName, taskKey);
				ICriteria taskEventListCriteria =
					GetListScheduledTaskEventsQuery(session, task.ID, startedDateFrom,
					startedDateTo, taskState, isManual);
				taskEventListCriteria.SetProjection(Projections.Count(TaskEvent.IDProp));
				int result = taskEventListCriteria.UniqueResult<int>();
				return result;
			}
		}

		/// <summary>
		/// Поставить задачу в очередь на выполнение
		/// </summary>
		/// <param name="systemName">система задачи</param>
		/// <param name="taskKey">ключ запускаемой задачи</param>
		/// <param name="userLogin">логин пользователя, инициировавшего запуск</param>
		public void StartTask(string systemName, string taskKey, string userLogin)
		{
			using (ISession session = this.OpenSession())
			{
				TaskData task = this.GetTaskData(session, systemName, taskKey);
				TaskEvent taskEvent = new TaskEvent(TaskEventState.Created,
					DateTime.Now, DateTime.Now, 0, DateTime.Now.AddSeconds(task.ManualLifeTime),
					null, DateTime.Now, userLogin, true, null, task.ID, null, task.System);

				session.Save(taskEvent);
				session.Flush();
			}
		}

		/// <summary>
		/// Убрать задачу из очереди на выполнение
		/// </summary>
		/// <param name="systemName"></param>
		/// <param name="taskKey"></param>
		public void StopTask(string systemName, string taskKey)
		{
			using (ISession session = this.OpenSession())
			{
				TaskData task = GetTaskData(session, systemName, taskKey);
				List<TaskEventState> runningStateList = new List<TaskEventState>();
				runningStateList.Add(TaskEventState.Created);
				runningStateList.Add(TaskEventState.Running);
				runningStateList.Add(TaskEventState.Delayed);
				IList<TaskEvent> runningTaskEventList = session.CreateCriteria(typeof(TaskEvent))
					.Add(Expression.Le(TaskEvent.ScheduledTimeProp, DateTime.Now))
					.Add(Expression.Eq(TaskEvent.TaskIDProp, task.ID))
					.Add(Expression.In(TaskEvent.StateProp, runningStateList))
					.List<TaskEvent>();
				foreach (TaskEvent taskEvent in runningTaskEventList)
				{
					taskEvent.State = TaskEventState.Cancelled;
					session.Update(taskEvent);
				}
				session.Flush();
			}
		}

		/// <summary>
		/// Получить расписание по его ID
		/// </summary>
		/// <param name="scheduleID"></param>
		public TaskSchedule GetTaskScheduleByID(int scheduleID)
		{
			using (ISession session = this.OpenSession())
			{
				TaskSchedule schedule = session.Get<TaskSchedule>(scheduleID);
				if (schedule == null)
					throw new Exception("расписание не найдено");

				return schedule;
			}
		}

		/// <summary>
		/// Сохранить расписание
		/// </summary>
		/// <param name="schedule"></param>
		/// <param name="systemName"></param>
		/// <param name="taskKey"></param>
		public void SaveSchedule(string systemName, string taskKey, TaskSchedule schedule)
		{
#warning irv 2008-03-09 подумать и переделать
			using (ISession session = this.OpenSession())
			{
				TaskData task = GetTaskData(session, systemName, taskKey);
				if (schedule.ID > 0)
				{
					TaskSchedule oldSchedule = session.Get<TaskSchedule>(schedule.ID);
					if (oldSchedule == null)
						throw new Exception("расписание не найдено");
					schedule.System = task.System;
					schedule.TaskID = task.ID;
					session.SaveOrUpdateCopy(schedule);
					session.Update(oldSchedule);
				}
				else
				{
					schedule.TaskID = task.ID;
					schedule.Task = task;
					schedule.System = task.System;
					session.SaveOrUpdate(schedule);
				}
				session.Flush();
				try
				{
					// обновить расписания при сохранении.
					ScheduleBuilder.RefreshSchedules(session);
				}
				catch (Exception ex)
				{
					throw new Exception(string.Format("расписание было сохранено, но при обновлении журнала запусков произошла ошибка: {0}.", ex));
				}
			}
		}

		/// <summary>
		/// Удалить расписание
		/// </summary>
		public void DeleteSchedule(int scheduleID)
		{
			using (ISession session = this.OpenSession())
			{
				TaskSchedule schedule = session.Get<TaskSchedule>(scheduleID);
				if (schedule == null)
					throw new Exception("расписание не найдено");
				session.Delete(schedule);
				session.Flush();
			}
		}

    }
}
